/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package cz.mzk.kramerius.articles.importFromLn.krameriusAccess;

import cz.mzk.kramerius.articles.importFromLn.PropertyNames;
import cz.mzk.kramerius.articles.importFromLn.domain.Issue;
import cz.mzk.kramerius.articles.importFromLn.updates.ArticleUpdater;
import cz.mzk.kramerius.commons.DatastreamId;
import cz.mzk.kramerius.commons.Dom4jUtils;
import cz.mzk.kramerius.commons.PropertyLoader;
import cz.mzk.kramerius.fedoraaccess.FedoraConnector;
import cz.mzk.kramerius.fedoraaccess.FedoraServer;
import cz.mzk.kramerius.fedoraaccess.rest.RESTConnector;
import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.joda.time.LocalDate;

/**
 * This class serves as access point to Fedora repository
 *
 * @author Martin Řehánek (rehan@mzk.cz)
 */
public class Repository {

    private static final String RELATIONS_NS_PREFIX = "kramerius";
    private static final Logger logger = Logger.getLogger(Repository.class.getName());
    private static final String INTERNAL_PART_RELATIONSHIP = "hasIntCompPart";
    private static Repository instance;
    private final Set<Integer> missingVolumesNumbers = new HashSet<Integer>();
    //private Map<String, PeriodicalAccess> periodicalMap; //key = periodicalMap PID
    private PeriodicalAccess periodicalAccess;
    private Map<String, VolumeAccess> volumeMap; //key = volume PID
    private Map<String, IssueAccess> issueMap; //key = issue PID
    private FedoraConnector connector;
    private ArticleUpdater articleUpdater = null;

    /**
     * The only method for retrieving instances of this class. This class is singleton, i.e. there
     * exists only one instance of the class.
     *
     * @param properties
     * @return instance of Repository or null in case of some error
     */
    public static Repository instanceOf(PropertyLoader properties) {
        if (instance == null) {
            instance = loadRepository(properties);
        }
        return instance;
    }

    private static Repository loadRepository(PropertyLoader properties) {
        try {
            String periodicalPid = properties.loadString(PropertyNames.PERIODICAL_PID);
            String fedoraUrl = properties.loadString(PropertyNames.FEDORA_URL);
            String fedoraLogin = properties.loadString(PropertyNames.FEDORA_LOGIN);
            String fedoraPass = properties.loadString(PropertyNames.FEDORA_PASSWORD);
            FedoraConnector con = new RESTConnector(new FedoraServer(fedoraUrl, fedoraLogin, fedoraPass));
            return new Repository(periodicalPid, con);
        } catch (Exception e) {
            logger.log(Level.SEVERE, null, e);
            return null;
        }
    }

    private Repository(String periodicalPid, FedoraConnector connector) {
        volumeMap = new HashMap<String, VolumeAccess>();
        issueMap = new HashMap<String, IssueAccess>();
        this.connector = connector;
        logger.log(Level.INFO, "Creating new instance of ''{0}''", PeriodicalAccess.class.getName());
        periodicalAccess = new PeriodicalAccessImpl(periodicalPid, connector);
    }

    /**
     * Finds PID of volume with specified number within a periodical
     *
     * @param volumeNumber number of volume within the given periodical
     * @return PID of volume digital object or null if there is no volume digital object with number
     * {@code volumeNumber}
     */
    public String getVolumePid(Integer volumeNumber) {
        String volumePid = periodicalAccess.getVolumePid(volumeNumber);
        if (volumePid == null) {
            missingVolumesNumbers.add(volumeNumber);
        }
        return volumePid;
    }

    /**
     * Finds year of volume with specified number within a periodical
     *
     * @param volumeNumber number of volume within the given periodical
     * @return year of volume digital object or null if there is no volume digital object with
     * number {@code volumeNumber}
     */
    public Integer getVolumeYear(int volumeNumber) {
        return periodicalAccess.getVolumeYear(volumeNumber);
    }

    /**
     * Finds PID of issue digital object (if exists) that has same dayTime and number within volume
     *
     * @param volumePid PID of volume digital object
     * @param volumeNumber number of volume
     * @param issue Issue object whose {@code getNumber()}, {@code getDayTime()} and
     * {@code getDate()} agrees with number and dayTime of issue being searched for
     * @return PID of issue digital object or null if none is found
     */
    public String getIssuePid(String volumePid, Integer volumeNumber, Issue issue) {
        VolumeAccess volume = volumeMap.get(volumePid);
        if (volume == null) {
            volume = initVolumeAccess(volumePid, volumeNumber);
        }
        return volume.getIssuePid(issue);
    }

    /**
     *
     * @param volumeNumber
     * @param issueNumber
     * @return date of issue with given number and volume number. Or null if volume or issue is not
     * found. There may still be more than one issues in that day but that changes nothing.
     */
    public LocalDate getIssueDate(Integer volumeNumber, Integer issueNumber) {
        String volumePid = periodicalAccess.getVolumePid(volumeNumber);
        if (volumePid == null) { //could not find volume
            return null;
        } else {
            VolumeAccess volume = volumeMap.get(volumePid);
            if (volume == null) {
                volume = initVolumeAccess(volumePid, volumeNumber);
            }
            return volume.getIssueDate(issueNumber);
        }

    }

    private VolumeAccess initVolumeAccess(String volumePid, Integer volumeNumber) {
        if (volumePid == null) {
            throw new NullPointerException("volumePid");
        }
        logger.log(Level.INFO, "Creating new instance of ''{0}'' for volume with pid ''{1}''", new Object[]{VolumeAccess.class.getName(), volumePid});
        VolumeAccess volume = new VolumeAccessImpl(volumePid, volumeNumber, connector);
        volumeMap.put(volumePid, volume);
        return volume;
    }

    /**
     * Finds PID of page digital object with given number within given issue
     *
     * @param issuePid PID of issue digital object that contains the page
     * @param pageNumber number of page within issue
     * @return PID of the page or null if there is no such a page
     */
    public String getPagePid(String issuePid, Integer pageNumber) {
        IssueAccess issue = issueMap.get(issuePid);
        if (issue == null) {
            issue = initIssueAccess(issuePid);
        }
        return issue.getPagePid(pageNumber);
    }

    /**
     *
     * @param issuePid PID of issue digital object that contains the pages
     * @return Collection of PIDs of all page digital objects from the issue
     */
    public Collection<String> getPagePidCollection(String issuePid) {
        IssueAccess issue = issueMap.get(issuePid);
        if (issue == null) {
            issue = initIssueAccess(issuePid);
        }
        return issue.getPagePidCollection();
    }

    private IssueAccess initIssueAccess(String issuePid) {
        logger.log(Level.INFO, "Creating new instance of ''{0}'' for issue with pid ''{1}''", new Object[]{IssueAccess.class.getName(), issuePid});
        IssueAccess issue = new IssueAccessImpl(issuePid, connector);
        issueMap.put(issuePid, issue);
        return issue;
    }

    /**
     * Prints summary about Fedora repository instance to the standard output
     */
    public void printRepositoryInfo() {
        connector.printRepositoryInfo();
    }

    public String importFoxml(Document doc, String importLogMessage) {
        return connector.importFoxml(doc, importLogMessage);
    }

    /**
     * Removes article and all relationships from the repository
     *
     * @param articlePid PID of article
     * @param volumePids Set of PIDs of volumes that contain article (usually just one)
     * @param issuePids Set of PIDs of issues that contain article
     */
    public void removeArticle(String articlePid, Set<String> volumePids, Set<String> issuePids) {
        removeVolumeArticleRelationships(articlePid, volumePids);
        removeIssueArticleRelationships(articlePid, issuePids);
        removeArticle(articlePid, "removing article");
    }

    /**
     * Removes article and all issue-article relationships from the repository
     *
     * @param articlePid PID of article
     * @param issuePids Set of PIDs of issues that contain article
     */
    public void removeArticle(String articlePid, Set<String> issuePids) {
        removeIssueArticleRelationships(articlePid, issuePids);
        removeArticle(articlePid, "removing article");
    }

    public void removeArticle(String pid, String logMessage) {
        if (connector.objectExists(pid)) {
            connector.removeObject(pid, logMessage);
            logger.log(Level.INFO, "article with pid ''{0}'' was removed", pid);
        } else {
            System.out.println("pid: " + pid);
            logger.log(Level.WARNING, "cannot remove article with pid ''{0}'' - object not found in repository", pid);
        }
    }

    public boolean addVolumeArticleRelationships(String articlePid, Set<String> volumePids) {
        return addRelationships(articlePid, volumePids, INTERNAL_PART_RELATIONSHIP);
    }

    public void removeVolumeArticleRelationships(String articlePid, Set<String> volumePids) {
        removeRelationships(articlePid, volumePids, INTERNAL_PART_RELATIONSHIP);
    }

    public boolean addIssueArticleRelationships(String articlePid, Set<String> issuePids) {
        return addRelationships(articlePid, issuePids, INTERNAL_PART_RELATIONSHIP);
    }

    private void removeIssueArticleRelationships(String articlePid, Set<String> issuePids) {
        removeRelationships(articlePid, issuePids, INTERNAL_PART_RELATIONSHIP);
    }

    private boolean addRelationships(String object, Set<String> subjectPids, String relationship) {
        boolean success = true;
        for (String subject : subjectPids) {
            boolean added = connector.addRelationship(subject, relationship, RELATIONS_NS_PREFIX, object);
            success &= added;
            if (added) {
                logger.log(Level.INFO, "relationship ''{0}'' of object ''{1}'' and subject ''{2}'' was added", new Object[]{relationship, object, subject});
            } else {
                logger.log(Level.WARNING, "relationship ''{0}'' of object ''{1}'' and subject ''{2}'' could not be added", new Object[]{relationship, object, subject});
            }
        }
        return success;
    }

    private void removeRelationships(String object, Set<String> subjectPids, String relationship) {
        for (String subject : subjectPids) {
            boolean removed = connector.removeRelationship(subject, relationship, RELATIONS_NS_PREFIX, object);
            if (removed) {
                logger.log(Level.INFO, "relationship ''{0}'' of object ''{1}'' and subject ''{2}'' was removed", new Object[]{relationship, object, subject});
            } else {
                logger.log(Level.WARNING, "relationship ''{0}'' of object ''{1}'' and subject ''{2}'' could not be removed - no such relationship", new Object[]{relationship, object, subject});
            }
        }
    }

    public void printDataStream(String pid, DatastreamId dsId, boolean print, File outFile) {
        Document doc2 = connector.getXmlDatastreamDissemination(pid, dsId);
        if (print) {
            System.out.println(doc2.asXML().toString());
        }
        if (outFile != null) {
            Dom4jUtils.saveDocument(doc2, outFile);
            logger.log(Level.INFO, "printing datastream ''{0}'' into file ''{1}''", new Object[]{dsId, outFile.toString()});
        }
    }

    /**
     *
     * @return Document with description of errors revealed in repository
     */
    public Document getErrorsInRepository() {
        Document errorDoc = DocumentHelper.createDocument();
        Element root = errorDoc.addElement("errors");
        Element repository = root.addElement("repository");
        repository.addAttribute("url", connector.getRepositoryUrl());
        Element periodical = repository.addElement("periodical");
        periodical.addAttribute("pid", periodicalAccess.getPeriodicalPid());
        if (!missingVolumesNumbers.isEmpty()) {
            //builder.append("Missing volumes: \n");
            Element missingVolumes = periodical.addElement("missingVolumes");
            for (Integer volumeNumber : missingVolumesNumbers) {
                Element volume = missingVolumes.addElement("volume");
                volume.addAttribute("number", volumeNumber.toString());
            }
        }
        Collection<VolumeAccess> volumes = volumeMap.values();
        for (VolumeAccess volume : volumes) {
            Element volumeEl = periodical.addElement("volume");
            volumeEl.addAttribute("number", volume.getVolumeNumber().toString());
            volumeEl.addAttribute("pid", volume.getVolumePid());
            //missing issues
            Set<Issue> missingIssues = volume.getMissingIssues();
            Element missingIssuesEl = volumeEl.addElement("missingIssues");
            for (Issue issue : missingIssues) {
                Element missingIssue = missingIssuesEl.addElement("issue");
                missingIssue.addAttribute("number", Integer.toString(issue.getNumber()));
                missingIssue.addAttribute("dayTime", issue.getDayTime().toString());
                missingIssue.addAttribute("date", issue.getDate().toString("YYYY-dd-MM"));
            }

            //duplicate issues
            Map<Issue, List<String>> duplicateIssuePidMap = volume.getDuplicateIssuePidMap();
            Element duplicateIssuesEl = volumeEl.addElement("duplicateIssuePids");
            for (Issue issue : duplicateIssuePidMap.keySet()) {
                Element issueEl = duplicateIssuesEl.addElement("issue");
                issueEl.addAttribute("number", Integer.toString(issue.getNumber()));
                issueEl.addAttribute("dayTime", issue.getDayTime().toString());
                issueEl.addAttribute("date", issue.getDate().toString("YYYY-dd-MM"));
                for (String pid : duplicateIssuePidMap.get(issue)) {
                    issueEl.addAttribute("pid", pid);
                }
            }
        }
        return errorDoc;
    }

    public boolean updateArticleDatastream(String articlePid, DatastreamId dsId, String updateClassName) {
        ArticleUpdater updater = loadArticleUpdater();
        return updater.updateDatastream(articlePid, dsId, updateClassName);
    }

    private ArticleUpdater loadArticleUpdater() {
        if (articleUpdater == null) {
            articleUpdater = new ArticleUpdater(connector);
        }
        return articleUpdater;
    }
}
